/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.codecimpl;

import java.awt.Color;
import java.awt.Point;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.Vector;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import org.eclipse.imagen.media.codec.ImageCodec;
import org.eclipse.imagen.media.codec.ImageDecoderImpl;
import org.eclipse.imagen.media.codec.PNGDecodeParam;
import org.eclipse.imagen.media.codec.PNGEncodeParam;
import org.eclipse.imagen.media.codecimpl.util.ImagingException;

/** @since EA3 */
public class PNGImageDecoder extends ImageDecoderImpl {

    public PNGImageDecoder(InputStream input, PNGDecodeParam param) {
        super(input, param);
    }

    public RenderedImage decodeAsRenderedImage(int page) throws IOException {
        if (page != 0) {
            throw new IOException(JaiI18N.getString("PNGImageDecoder19"));
        }
        try {
            return new PNGImage(input, (PNGDecodeParam) param);
        } catch (Exception e) {
            throw CodecUtils.toIOException(e);
        }
    }
}

class PNGChunk {
    int length;
    int type;
    byte[] data;
    int crc;

    String typeString;

    public PNGChunk(int length, int type, byte[] data, int crc) {
        this.length = length;
        this.type = type;
        this.data = data;
        this.crc = crc;

        typeString = new String();
        typeString += (char) (type >> 24);
        typeString += (char) ((type >> 16) & 0xff);
        typeString += (char) ((type >> 8) & 0xff);
        typeString += (char) (type & 0xff);
    }

    public int getLength() {
        return length;
    }

    public int getType() {
        return type;
    }

    public String getTypeString() {
        return typeString;
    }

    public byte[] getData() {
        return data;
    }

    public byte getByte(int offset) {
        return data[offset];
    }

    public int getInt1(int offset) {
        return data[offset] & 0xff;
    }

    public int getInt2(int offset) {
        return ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff);
    }

    public int getInt4(int offset) {
        return ((data[offset] & 0xff) << 24)
                | ((data[offset + 1] & 0xff) << 16)
                | ((data[offset + 2] & 0xff) << 8)
                | (data[offset + 3] & 0xff);
    }

    public String getString4(int offset) {
        String s = new String();
        s += (char) data[offset];
        s += (char) data[offset + 1];
        s += (char) data[offset + 2];
        s += (char) data[offset + 3];
        return s;
    }

    public boolean isType(String typeName) {
        return typeString.equals(typeName);
    }
}

/**
 * TO DO:
 *
 * <p>zTXt chunks
 */
class PNGImage extends SimpleRenderedImage {

    public static final int PNG_COLOR_GRAY = 0;
    public static final int PNG_COLOR_RGB = 2;
    public static final int PNG_COLOR_PALETTE = 3;
    public static final int PNG_COLOR_GRAY_ALPHA = 4;
    public static final int PNG_COLOR_RGB_ALPHA = 6;

    private static final String[] colorTypeNames = {
        "Grayscale", "Error", "Truecolor", "Index", "Grayscale with alpha", "Error", "Truecolor with alpha"
    };

    public static final int PNG_FILTER_NONE = 0;
    public static final int PNG_FILTER_SUB = 1;
    public static final int PNG_FILTER_UP = 2;
    public static final int PNG_FILTER_AVERAGE = 3;
    public static final int PNG_FILTER_PAETH = 4;

    private static final int RED_OFFSET = 2;
    private static final int GREEN_OFFSET = 1;
    private static final int BLUE_OFFSET = 0;

    private int[][] bandOffsets = {
        null,
        {0}, // G
        {0, 1}, // GA in GA order
        {0, 1, 2}, // RGB in RGB order
        {0, 1, 2, 3} // RGBA in RGBA order
    };

    private int bitDepth;
    private int colorType;

    private int compressionMethod;
    private int filterMethod;
    private int interlaceMethod;

    private int paletteEntries;
    private byte[] redPalette;
    private byte[] greenPalette;
    private byte[] bluePalette;
    private byte[] alphaPalette;

    private int bkgdRed;
    private int bkgdGreen;
    private int bkgdBlue;

    private int grayTransparentAlpha;
    private int redTransparentAlpha;
    private int greenTransparentAlpha;
    private int blueTransparentAlpha;

    private int maxOpacity;

    private int[] significantBits = null;

    private boolean hasBackground = false;

    // Parameter information

    // If true, the user wants destination alpha where applicable.
    private boolean suppressAlpha = false;

    // If true, perform palette lookup internally
    private boolean expandPalette = false;

    // If true, output < 8 bit gray images in 8 bit components format
    private boolean output8BitGray = false;

    // Create an alpha channel in the destination color model.
    private boolean outputHasAlphaPalette = false;

    // Perform gamma correction on the image
    private boolean performGammaCorrection = false;

    // Expand GA to GGGA for compatbility with Java2D
    private boolean expandGrayAlpha = false;

    // Produce an instance of PNGEncodeParam
    private boolean generateEncodeParam = false;

    // PNGDecodeParam controlling decode process
    private PNGDecodeParam decodeParam = null;

    // PNGEncodeParam to store file details in
    private PNGEncodeParam encodeParam = null;

    private boolean emitProperties = true;

    private float fileGamma = 45455 / 100000.0F;

    private float userExponent = 1.0F;

    private float displayExponent = 2.2F;

    private float[] chromaticity = null;

    private int sRGBRenderingIntent = -1;

    // ICCP parameters
    private ICC_Profile iccProfile = null;
    private String iccProfileName = null;

    // Post-processing step implied by above parameters
    private int postProcess = POST_NONE;

    // Possible post-processing steps

    // Do nothing
    private static final int POST_NONE = 0;

    // Gamma correct only
    private static final int POST_GAMMA = 1;

    // Push gray values through grayLut to expand to 8 bits
    private static final int POST_GRAY_LUT = 2;

    // Push gray values through grayLut to expand to 8 bits, add alpha
    private static final int POST_GRAY_LUT_ADD_TRANS = 3;

    // Push palette value through R,G,B lookup tables
    private static final int POST_PALETTE_TO_RGB = 4;

    // Push palette value through R,G,B,A lookup tables
    private static final int POST_PALETTE_TO_RGBA = 5;

    // Add transparency to a given gray value (w/ optional gamma)
    private static final int POST_ADD_GRAY_TRANS = 6;

    // Add transparency to a given RGB value (w/ optional gamma)
    private static final int POST_ADD_RGB_TRANS = 7;

    // Remove the alpha channel from a gray image (w/ optional gamma)
    private static final int POST_REMOVE_GRAY_TRANS = 8;

    // Remove the alpha channel from an RGB image (w/optional gamma)
    private static final int POST_REMOVE_RGB_TRANS = 9;

    // Mask to add expansion of GA -> GGGA
    private static final int POST_EXP_MASK = 16;

    // Expand gray to G/G/G
    private static final int POST_GRAY_ALPHA_EXP = POST_NONE | POST_EXP_MASK;

    // Expand gray to G/G/G through a gamma lut
    private static final int POST_GAMMA_EXP = POST_GAMMA | POST_EXP_MASK;

    // Push gray values through grayLut to expand to 8 bits, expand, add alpha
    private static final int POST_GRAY_LUT_ADD_TRANS_EXP = POST_GRAY_LUT_ADD_TRANS | POST_EXP_MASK;

    // Add transparency to a given gray value, expand
    private static final int POST_ADD_GRAY_TRANS_EXP = POST_ADD_GRAY_TRANS | POST_EXP_MASK;

    private Vector streamVec = new Vector();
    private DataInputStream dataStream;

    private int bytesPerPixel; // number of bytes per input pixel
    private int inputBands;
    private int outputBands;

    // Number of private chunks
    private int chunkIndex = 0;

    private Vector textKeys = new Vector();
    private Vector textStrings = new Vector();

    private Vector ztextKeys = new Vector();
    private Vector ztextStrings = new Vector();

    private WritableRaster theTile;

    private int[] gammaLut = null;

    private void initGammaLut(int bits) {
        double exp = (double) userExponent / (fileGamma * displayExponent);
        int numSamples = 1 << bits;
        int maxOutSample = (bits == 16) ? 65535 : 255;

        gammaLut = new int[numSamples];
        for (int i = 0; i < numSamples; i++) {
            double gbright = (double) i / (numSamples - 1);
            double gamma = Math.pow(gbright, exp);
            int igamma = (int) (gamma * maxOutSample + 0.5);
            if (igamma > maxOutSample) {
                igamma = maxOutSample;
            }
            gammaLut[i] = igamma;
        }
    }

    private final byte[][] expandBits = {
        null,
        {(byte) 0x00, (byte) 0xff},
        {(byte) 0x00, (byte) 0x55, (byte) 0xaa, (byte) 0xff},
        null,
        {
            (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
            (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77,
            (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb,
            (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff
        }
    };

    private int[] grayLut = null;

    private void initGrayLut(int bits) {
        int len = 1 << bits;
        grayLut = new int[len];

        if (performGammaCorrection) {
            for (int i = 0; i < len; i++) {
                grayLut[i] = gammaLut[i];
            }
        } else {
            for (int i = 0; i < len; i++) {
                grayLut[i] = expandBits[bits][i];
            }
        }
    }

    public PNGImage(InputStream stream, PNGDecodeParam decodeParam) throws IOException {

        if (!stream.markSupported()) {
            stream = new BufferedInputStream(stream);
        }
        DataInputStream distream = new DataInputStream(stream);

        if (decodeParam == null) {
            decodeParam = new PNGDecodeParam();
        }
        this.decodeParam = decodeParam;

        // Get parameter values
        this.suppressAlpha = decodeParam.getSuppressAlpha();
        this.expandPalette = decodeParam.getExpandPalette();
        this.output8BitGray = decodeParam.getOutput8BitGray();
        this.expandGrayAlpha = decodeParam.getExpandGrayAlpha();
        if (decodeParam.getPerformGammaCorrection()) {
            this.userExponent = decodeParam.getUserExponent();
            this.displayExponent = decodeParam.getDisplayExponent();
            performGammaCorrection = true;
            output8BitGray = true;
        }
        this.generateEncodeParam = decodeParam.getGenerateEncodeParam();

        if (emitProperties) {
            properties.put("file_type", "PNG v. 1.0");
        }

        try {
            long magic = distream.readLong();
            if (magic != 0x89504e470d0a1a0aL) {
                String msg = JaiI18N.getString("PNGImageDecoder0");
                throw new RuntimeException(msg);
            }
        } catch (Exception e) {
            String message = JaiI18N.getString("PNGImageDecoder1");
            ImagingListenerProxy.errorOccurred(message, new ImagingException(message, e), this, false);
            /*
                        e.printStackTrace();
                        String msg = JaiI18N.getString("PNGImageDecoder1");
                        throw new RuntimeException(msg);
            */
        }

        do {
            try {
                PNGChunk chunk;

                String chunkType = getChunkType(distream);
                if (chunkType.equals("IHDR")) {
                    chunk = readChunk(distream);
                    parse_IHDR_chunk(chunk);
                } else if (chunkType.equals("PLTE")) {
                    chunk = readChunk(distream);
                    parse_PLTE_chunk(chunk);
                } else if (chunkType.equals("IDAT")) {
                    chunk = readChunk(distream);
                    streamVec.add(new ByteArrayInputStream(chunk.getData()));
                } else if (chunkType.equals("IEND")) {
                    chunk = readChunk(distream);
                    parse_IEND_chunk(chunk);
                    break; // fall through to the bottom
                } else if (chunkType.equals("bKGD")) {
                    chunk = readChunk(distream);
                    parse_bKGD_chunk(chunk);
                } else if (chunkType.equals("cHRM")) {
                    chunk = readChunk(distream);
                    parse_cHRM_chunk(chunk);
                } else if (chunkType.equals("gAMA")) {
                    chunk = readChunk(distream);
                    parse_gAMA_chunk(chunk);
                } else if (chunkType.equals("hIST")) {
                    chunk = readChunk(distream);
                    parse_hIST_chunk(chunk);
                } else if (chunkType.equals("iCCP")) {
                    chunk = readChunk(distream);
                    parse_iCCP_chunk(chunk);
                } else if (chunkType.equals("pHYs")) {
                    chunk = readChunk(distream);
                    parse_pHYs_chunk(chunk);
                } else if (chunkType.equals("sBIT")) {
                    chunk = readChunk(distream);
                    parse_sBIT_chunk(chunk);
                } else if (chunkType.equals("sRGB")) {
                    chunk = readChunk(distream);
                    parse_sRGB_chunk(chunk);
                } else if (chunkType.equals("tEXt")) {
                    chunk = readChunk(distream);
                    parse_tEXt_chunk(chunk);
                } else if (chunkType.equals("tIME")) {
                    chunk = readChunk(distream);
                    parse_tIME_chunk(chunk);
                } else if (chunkType.equals("tRNS")) {
                    chunk = readChunk(distream);
                    parse_tRNS_chunk(chunk);
                } else if (chunkType.equals("zTXt")) {
                    chunk = readChunk(distream);
                    parse_zTXt_chunk(chunk);
                } else {
                    chunk = readChunk(distream);
                    // Output the chunk data in raw form

                    String type = chunk.getTypeString();
                    byte[] data = chunk.getData();
                    if (encodeParam != null) {
                        encodeParam.addPrivateChunk(type, data);
                    }
                    if (emitProperties) {
                        String key = "chunk_" + chunkIndex++ + ":" + type;
                        properties.put(key.toLowerCase(), data);
                    }
                }
            } catch (Exception e) {
                String message = JaiI18N.getString("PNGImageDecoder2");
                ImagingListenerProxy.errorOccurred(message, new ImagingException(message, e), this, false);
                /*                e.printStackTrace();
                                String msg = JaiI18N.getString("PNGImageDecoder2");
                                throw new RuntimeException(msg);
                */
            }
        } while (true);

        // Final post-processing

        if (significantBits == null) {
            significantBits = new int[inputBands];
            for (int i = 0; i < inputBands; i++) {
                significantBits[i] = bitDepth;
            }

            if (emitProperties) {
                properties.put("significant_bits", significantBits);
            }
        }
    }

    private static String getChunkType(DataInputStream distream) {
        try {
            distream.mark(8);
            int length = distream.readInt();
            int type = distream.readInt();
            distream.reset();

            String typeString = new String();
            typeString += (char) (type >> 24);
            typeString += (char) ((type >> 16) & 0xff);
            typeString += (char) ((type >> 8) & 0xff);
            typeString += (char) (type & 0xff);
            return typeString;
        } catch (Exception e) {
            ImagingListenerProxy.errorOccurred(JaiI18N.getString("PNGImageDecoder20"), e, PNGImageDecoder.class, false);
            //            e.printStackTrace();
            return null;
        }
    }

    private static PNGChunk readChunk(DataInputStream distream) {
        try {
            int length = distream.readInt();
            int type = distream.readInt();
            byte[] data = new byte[length];
            distream.readFully(data);
            int crc = distream.readInt();

            return new PNGChunk(length, type, data, crc);
        } catch (Exception e) {
            ImagingListenerProxy.errorOccurred(JaiI18N.getString("PNGImageDecoder21"), e, PNGImageDecoder.class, false);
            //            e.printStackTrace();
            return null;
        }
    }

    private void parse_IHDR_chunk(PNGChunk chunk) {
        tileWidth = width = chunk.getInt4(0);
        tileHeight = height = chunk.getInt4(4);

        bitDepth = chunk.getInt1(8);

        if ((bitDepth != 1) && (bitDepth != 2) && (bitDepth != 4) && (bitDepth != 8) && (bitDepth != 16)) {
            // Error -- bad bit depth
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder3"));
        }
        maxOpacity = (1 << bitDepth) - 1;

        colorType = chunk.getInt1(9);
        if ((colorType != PNG_COLOR_GRAY)
                && (colorType != PNG_COLOR_RGB)
                && (colorType != PNG_COLOR_PALETTE)
                && (colorType != PNG_COLOR_GRAY_ALPHA)
                && (colorType != PNG_COLOR_RGB_ALPHA)) {
            System.out.println(JaiI18N.getString("PNGImageDecoder4"));
        }

        if ((colorType == PNG_COLOR_RGB) && (bitDepth < 8)) {
            // Error -- RGB images must have 8 or 16 bits
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder5"));
        }

        if ((colorType == PNG_COLOR_PALETTE) && (bitDepth == 16)) {
            // Error -- palette images must have < 16 bits
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder6"));
        }

        if ((colorType == PNG_COLOR_GRAY_ALPHA) && (bitDepth < 8)) {
            // Error -- gray/alpha images must have >= 8 bits
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder7"));
        }

        if ((colorType == PNG_COLOR_RGB_ALPHA) && (bitDepth < 8)) {
            // Error -- RGB/alpha images must have >= 8 bits
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder8"));
        }

        if (emitProperties) {
            properties.put("color_type", colorTypeNames[colorType]);
        }

        if (generateEncodeParam) {
            if (colorType == PNG_COLOR_PALETTE) {
                encodeParam = new PNGEncodeParam.Palette();
            } else if (colorType == PNG_COLOR_GRAY || colorType == PNG_COLOR_GRAY_ALPHA) {
                encodeParam = new PNGEncodeParam.Gray();
            } else {
                encodeParam = new PNGEncodeParam.RGB();
            }
            decodeParam.setEncodeParam(encodeParam);
        }

        if (encodeParam != null) {
            encodeParam.setBitDepth(bitDepth);
        }
        if (emitProperties) {
            properties.put("bit_depth", new Integer(bitDepth));
        }

        if (performGammaCorrection) {
            // Assume file gamma is 1/2.2 unless we get a gAMA chunk
            float gamma = (1.0F / 2.2F) * (displayExponent / userExponent);
            if (encodeParam != null) {
                encodeParam.setGamma(gamma);
            }
            if (emitProperties) {
                properties.put("gamma", new Float(gamma));
            }
        }

        compressionMethod = chunk.getInt1(10);
        if (compressionMethod != 0) {
            // Error -- only know about compression method 0
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder9"));
        }

        filterMethod = chunk.getInt1(11);
        if (filterMethod != 0) {
            // Error -- only know about filter method 0
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder10"));
        }

        interlaceMethod = chunk.getInt1(12);
        if (interlaceMethod == 0) {
            if (encodeParam != null) {
                encodeParam.setInterlacing(false);
            }
            if (emitProperties) {
                properties.put("interlace_method", "None");
            }
        } else if (interlaceMethod == 1) {
            if (encodeParam != null) {
                encodeParam.setInterlacing(true);
            }
            if (emitProperties) {
                properties.put("interlace_method", "Adam7");
            }
        } else {
            // Error -- only know about Adam7 interlacing
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder11"));
        }

        bytesPerPixel = (bitDepth == 16) ? 2 : 1;

        switch (colorType) {
            case PNG_COLOR_GRAY:
                inputBands = 1;
                outputBands = 1;

                if (output8BitGray && (bitDepth < 8)) {
                    postProcess = POST_GRAY_LUT;
                } else if (performGammaCorrection) {
                    postProcess = POST_GAMMA;
                } else {
                    postProcess = POST_NONE;
                }
                break;

            case PNG_COLOR_RGB:
                inputBands = 3;
                bytesPerPixel *= 3;
                outputBands = 3;

                if (performGammaCorrection) {
                    postProcess = POST_GAMMA;
                } else {
                    postProcess = POST_NONE;
                }
                break;

            case PNG_COLOR_PALETTE:
                inputBands = 1;
                bytesPerPixel = 1;
                outputBands = expandPalette ? 3 : 1;

                if (expandPalette) {
                    postProcess = POST_PALETTE_TO_RGB;
                } else {
                    postProcess = POST_NONE;
                }
                break;

            case PNG_COLOR_GRAY_ALPHA:
                inputBands = 2;
                bytesPerPixel *= 2;

                if (suppressAlpha) {
                    outputBands = 1;
                    postProcess = POST_REMOVE_GRAY_TRANS;
                } else {
                    if (performGammaCorrection) {
                        postProcess = POST_GAMMA;
                    } else {
                        postProcess = POST_NONE;
                    }
                    if (expandGrayAlpha) {
                        postProcess |= POST_EXP_MASK;
                        outputBands = 4;
                    } else {
                        outputBands = 2;
                    }
                }
                break;

            case PNG_COLOR_RGB_ALPHA:
                inputBands = 4;
                bytesPerPixel *= 4;
                outputBands = (!suppressAlpha) ? 4 : 3;

                if (suppressAlpha) {
                    postProcess = POST_REMOVE_RGB_TRANS;
                } else if (performGammaCorrection) {
                    postProcess = POST_GAMMA;
                } else {
                    postProcess = POST_NONE;
                }
                break;
        }
    }

    private void parse_IEND_chunk(PNGChunk chunk) throws Exception {
        // Store text strings
        int textLen = textKeys.size();
        String[] textArray = new String[2 * textLen];
        for (int i = 0; i < textLen; i++) {
            String key = (String) textKeys.elementAt(i);
            String val = (String) textStrings.elementAt(i);
            textArray[2 * i] = key;
            textArray[2 * i + 1] = val;
            if (emitProperties) {
                String uniqueKey = "text_" + i + ":" + key;
                properties.put(uniqueKey.toLowerCase(), val);
            }
        }
        if (encodeParam != null) {
            encodeParam.setText(textArray);
        }

        // Store compressed text strings
        int ztextLen = ztextKeys.size();
        String[] ztextArray = new String[2 * ztextLen];
        for (int i = 0; i < ztextLen; i++) {
            String key = (String) ztextKeys.elementAt(i);
            String val = (String) ztextStrings.elementAt(i);
            ztextArray[2 * i] = key;
            ztextArray[2 * i + 1] = val;
            if (emitProperties) {
                String uniqueKey = "ztext_" + i + ":" + key;
                properties.put(uniqueKey.toLowerCase(), val);
            }
        }
        if (encodeParam != null) {
            encodeParam.setCompressedText(ztextArray);
        }

        // detect sRGB & iCCP conflict
        if (sRGBRenderingIntent != -1 && iccProfile != null) {
            // resolve by dropping ICC Profile
            iccProfile = null;
        }

        // add iccProfile to encodeParam
        if (encodeParam != null && iccProfile != null) {
            encodeParam.setICCProfileData(iccProfile.getData());
            encodeParam.setICCProfileName(iccProfileName);
        }

        // Parse prior IDAT chunks
        InputStream seqStream = new SequenceInputStream(streamVec.elements());
        InputStream infStream = new InflaterInputStream(seqStream, new Inflater());
        dataStream = new DataInputStream(infStream);

        // Create an empty WritableRaster
        int depth = bitDepth;
        if ((colorType == PNG_COLOR_GRAY) && (bitDepth < 8) && output8BitGray) {
            depth = 8;
        }
        if ((colorType == PNG_COLOR_PALETTE) && expandPalette) {
            depth = 8;
        }
        int bytesPerRow = (outputBands * width * depth + 7) / 8;
        int scanlineStride = (depth == 16) ? (bytesPerRow / 2) : bytesPerRow;

        theTile = createRaster(width, height, outputBands, scanlineStride, depth);

        if (performGammaCorrection && (gammaLut == null)) {
            initGammaLut(bitDepth);
        }
        if ((postProcess == POST_GRAY_LUT)
                || (postProcess == POST_GRAY_LUT_ADD_TRANS)
                || (postProcess == POST_GRAY_LUT_ADD_TRANS_EXP)) {
            initGrayLut(bitDepth);
        }

        decodeImage(interlaceMethod == 1);
        sampleModel = theTile.getSampleModel();

        if ((colorType == PNG_COLOR_PALETTE) && !expandPalette) {
            if (outputHasAlphaPalette) {
                colorModel = new IndexColorModel(
                        bitDepth, paletteEntries, redPalette, greenPalette, bluePalette, alphaPalette);
            } else {
                colorModel = new IndexColorModel(bitDepth, paletteEntries, redPalette, greenPalette, bluePalette);
            }
        } else if ((colorType == PNG_COLOR_GRAY) && (bitDepth < 8) && !output8BitGray) {
            byte[] palette = expandBits[bitDepth];
            colorModel = new IndexColorModel(bitDepth, palette.length, palette, palette, palette);
        } else {
            colorModel = ImageCodec.createComponentColorModel(
                    sampleModel, iccProfile == null ? null : new ICC_ColorSpace(iccProfile));
        }
    }

    private void parse_PLTE_chunk(PNGChunk chunk) {
        paletteEntries = chunk.getLength() / 3;
        redPalette = new byte[paletteEntries];
        greenPalette = new byte[paletteEntries];
        bluePalette = new byte[paletteEntries];

        int pltIndex = 0;

        // gAMA chunk must precede PLTE chunk
        if (performGammaCorrection) {
            if (gammaLut == null) {
                initGammaLut(bitDepth == 16 ? 16 : 8);
            }

            for (int i = 0; i < paletteEntries; i++) {
                byte r = chunk.getByte(pltIndex++);
                byte g = chunk.getByte(pltIndex++);
                byte b = chunk.getByte(pltIndex++);

                redPalette[i] = (byte) gammaLut[r & 0xff];
                greenPalette[i] = (byte) gammaLut[g & 0xff];
                bluePalette[i] = (byte) gammaLut[b & 0xff];
            }
        } else {
            for (int i = 0; i < paletteEntries; i++) {
                redPalette[i] = chunk.getByte(pltIndex++);
                greenPalette[i] = chunk.getByte(pltIndex++);
                bluePalette[i] = chunk.getByte(pltIndex++);
            }
        }
    }

    private void parse_bKGD_chunk(PNGChunk chunk) {
        hasBackground = true;

        switch (colorType) {
            case PNG_COLOR_PALETTE:
                int bkgdIndex = chunk.getByte(0) & 0xff;

                bkgdRed = redPalette[bkgdIndex] & 0xff;
                bkgdGreen = greenPalette[bkgdIndex] & 0xff;
                bkgdBlue = bluePalette[bkgdIndex] & 0xff;

                if (encodeParam != null) {
                    ((PNGEncodeParam.Palette) encodeParam).setBackgroundPaletteIndex(bkgdIndex);
                }
                break;
            case PNG_COLOR_GRAY:
            case PNG_COLOR_GRAY_ALPHA:
                int bkgdGray = chunk.getInt2(0);
                bkgdRed = bkgdGreen = bkgdBlue = bkgdGray;

                if (encodeParam != null) {
                    ((PNGEncodeParam.Gray) encodeParam).setBackgroundGray(bkgdGray);
                }
                break;
            case PNG_COLOR_RGB:
            case PNG_COLOR_RGB_ALPHA:
                // Fix 4625294: In the case of bitDepth = 8,
                // when the background color values is larger
                // than 128, and the encoder copies the byte into a short
                // without masking, the decoded background values may be
                // out of 8 bit range.  So mask them here to avoid the
                // exception thrown by the constructor of Color.
                // So mask to make it safe even when the values exceeds
                // the range.
                int mask = (1 << bitDepth) - 1;
                bkgdRed = chunk.getInt2(0) & mask;
                bkgdGreen = chunk.getInt2(2) & mask;
                bkgdBlue = chunk.getInt2(4) & mask;

                int[] bkgdRGB = new int[3];
                bkgdRGB[0] = bkgdRed;
                bkgdRGB[1] = bkgdGreen;
                bkgdRGB[2] = bkgdBlue;
                if (encodeParam != null) {
                    ((PNGEncodeParam.RGB) encodeParam).setBackgroundRGB(bkgdRGB);
                }
                break;
        }

        int r = 0, g = 0, b = 0;
        if (bitDepth < 8) {
            r = expandBits[bitDepth][bkgdRed];
            g = expandBits[bitDepth][bkgdGreen];
            b = expandBits[bitDepth][bkgdBlue];
        } else if (bitDepth == 8) {
            r = bkgdRed;
            g = bkgdGreen;
            b = bkgdBlue;
        } else if (bitDepth == 16) {
            r = bkgdRed >> 8;
            g = bkgdGreen >> 8;
            b = bkgdBlue >> 8;
        }
        if (emitProperties) {
            properties.put("background_color", new Color(r, g, b));
        }
    }

    private void parse_cHRM_chunk(PNGChunk chunk) {
        // If an sRGB chunk exists, ignore cHRM chunks
        if (sRGBRenderingIntent != -1) {
            return;
        }

        chromaticity = new float[8];
        chromaticity[0] = chunk.getInt4(0) / 100000.0F;
        chromaticity[1] = chunk.getInt4(4) / 100000.0F;
        chromaticity[2] = chunk.getInt4(8) / 100000.0F;
        chromaticity[3] = chunk.getInt4(12) / 100000.0F;
        chromaticity[4] = chunk.getInt4(16) / 100000.0F;
        chromaticity[5] = chunk.getInt4(20) / 100000.0F;
        chromaticity[6] = chunk.getInt4(24) / 100000.0F;
        chromaticity[7] = chunk.getInt4(28) / 100000.0F;

        if (encodeParam != null) {
            encodeParam.setChromaticity(chromaticity);
        }
        if (emitProperties) {
            properties.put("white_point_x", new Float(chromaticity[0]));
            properties.put("white_point_y", new Float(chromaticity[1]));
            properties.put("red_x", new Float(chromaticity[2]));
            properties.put("red_y", new Float(chromaticity[3]));
            properties.put("green_x", new Float(chromaticity[4]));
            properties.put("green_y", new Float(chromaticity[5]));
            properties.put("blue_x", new Float(chromaticity[6]));
            properties.put("blue_y", new Float(chromaticity[7]));
        }
    }

    private void parse_gAMA_chunk(PNGChunk chunk) {
        // If an sRGB chunk exists, ignore gAMA chunks
        if (sRGBRenderingIntent != -1) {
            return;
        }

        fileGamma = chunk.getInt4(0) / 100000.0F;

        float exp = performGammaCorrection ? displayExponent / userExponent : 1.0F;
        if (encodeParam != null) {
            encodeParam.setGamma(fileGamma * exp);
        }
        if (emitProperties) {
            properties.put("gamma", new Float(fileGamma * exp));
        }
    }

    private void parse_hIST_chunk(PNGChunk chunk) {
        if (redPalette == null) {
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder18"));
        }

        int length = redPalette.length;
        int[] hist = new int[length];
        for (int i = 0; i < length; i++) {
            hist[i] = chunk.getInt2(2 * i);
        }

        if (encodeParam != null) {
            encodeParam.setPaletteHistogram(hist);
        }
    }

    private void parse_iCCP_chunk(PNGChunk chunk) {
        byte b;
        byte[] data = new byte[80];
        int pos = 0;
        while (pos < 79 && (b = chunk.getByte(pos)) != 0) {
            data[pos++] = b;
        }

        data[pos] = 0;
        String name = new String(data);
        byte compMethod = chunk.getByte(pos++);
        InflaterInputStream infls =
                new InflaterInputStream(new ByteArrayInputStream(chunk.getData(), pos, chunk.getLength() - pos));
        try {
            iccProfile = ICC_Profile.getInstance(infls);
            iccProfileName = name;
        } catch (IOException e) {
            iccProfile = null;
            iccProfileName = null;
        }
    }

    private void parse_pHYs_chunk(PNGChunk chunk) {
        int xPixelsPerUnit = chunk.getInt4(0);
        int yPixelsPerUnit = chunk.getInt4(4);
        int unitSpecifier = chunk.getInt1(8);

        if (encodeParam != null) {
            encodeParam.setPhysicalDimension(xPixelsPerUnit, yPixelsPerUnit, unitSpecifier);
        }
        if (emitProperties) {
            properties.put("x_pixels_per_unit", new Integer(xPixelsPerUnit));
            properties.put("y_pixels_per_unit", new Integer(yPixelsPerUnit));
            properties.put("pixel_aspect_ratio", new Float((float) xPixelsPerUnit / yPixelsPerUnit));
            if (unitSpecifier == 1) {
                properties.put("pixel_units", "Meters");
            } else if (unitSpecifier != 0) {
                // Error -- unit specifier must be 0 or 1
                throw new RuntimeException(JaiI18N.getString("PNGImageDecoder12"));
            }
        }
    }

    private void parse_sBIT_chunk(PNGChunk chunk) {
        if (colorType == PNG_COLOR_PALETTE) {
            significantBits = new int[3];
        } else {
            significantBits = new int[inputBands];
        }
        for (int i = 0; i < significantBits.length; i++) {
            int bits = (int) chunk.getByte(i);
            int depth = (colorType == PNG_COLOR_PALETTE) ? 8 : bitDepth;
            if (bits <= 0 || bits > depth) {
                // Error -- significant bits must be between 0 and
                // image bit depth.
                throw new RuntimeException(JaiI18N.getString("PNGImageDecoder13"));
            }
            significantBits[i] = bits;
        }

        if (encodeParam != null) {
            encodeParam.setSignificantBits(significantBits);
        }
        if (emitProperties) {
            properties.put("significant_bits", significantBits);
        }
    }

    private void parse_sRGB_chunk(PNGChunk chunk) {
        sRGBRenderingIntent = chunk.getByte(0);

        // The presence of an sRGB chunk implies particular
        // settings for gamma and chroma.
        fileGamma = 45455 / 100000.0F;

        chromaticity = new float[8];
        chromaticity[0] = 31270 / 10000.0F;
        chromaticity[1] = 32900 / 10000.0F;
        chromaticity[2] = 64000 / 10000.0F;
        chromaticity[3] = 33000 / 10000.0F;
        chromaticity[4] = 30000 / 10000.0F;
        chromaticity[5] = 60000 / 10000.0F;
        chromaticity[6] = 15000 / 10000.0F;
        chromaticity[7] = 6000 / 10000.0F;

        if (performGammaCorrection) {
            // File gamma is 1/2.2
            float gamma = fileGamma * (displayExponent / userExponent);
            if (encodeParam != null) {
                encodeParam.setGamma(gamma);
                encodeParam.setChromaticity(chromaticity);
            }
            if (emitProperties) {
                properties.put("gamma", new Float(gamma));
                properties.put("white_point_x", new Float(chromaticity[0]));
                properties.put("white_point_y", new Float(chromaticity[1]));
                properties.put("red_x", new Float(chromaticity[2]));
                properties.put("red_y", new Float(chromaticity[3]));
                properties.put("green_x", new Float(chromaticity[4]));
                properties.put("green_y", new Float(chromaticity[5]));
                properties.put("blue_x", new Float(chromaticity[6]));
                properties.put("blue_y", new Float(chromaticity[7]));
            }
        }
    }

    private void parse_tEXt_chunk(PNGChunk chunk) {
        String key = new String();
        String value = new String();
        byte b;

        int textIndex = 0;
        while ((b = chunk.getByte(textIndex++)) != 0) {
            key += (char) b;
        }

        for (int i = textIndex; i < chunk.getLength(); i++) {
            value += (char) chunk.getByte(i);
        }

        textKeys.add(key);
        textStrings.add(value);
    }

    private void parse_tIME_chunk(PNGChunk chunk) {
        int year = chunk.getInt2(0);
        int month = chunk.getInt1(2) - 1;
        int day = chunk.getInt1(3);
        int hour = chunk.getInt1(4);
        int minute = chunk.getInt1(5);
        int second = chunk.getInt1(6);

        TimeZone gmt = TimeZone.getTimeZone("GMT");

        GregorianCalendar cal = new GregorianCalendar(gmt);
        cal.set(year, month, day, hour, minute, second);
        Date date = cal.getTime();

        if (encodeParam != null) {
            encodeParam.setModificationTime(date);
        }
        if (emitProperties) {
            properties.put("timestamp", date);
        }
    }

    private void parse_tRNS_chunk(PNGChunk chunk) {
        if (colorType == PNG_COLOR_PALETTE) {
            int entries = chunk.getLength();
            if (entries > paletteEntries) {
                // Error -- mustn't have more alpha than RGB palette entries
                throw new RuntimeException(JaiI18N.getString("PNGImageDecoder14"));
            }

            // Load beginning of palette from the chunk
            alphaPalette = new byte[paletteEntries];
            for (int i = 0; i < entries; i++) {
                alphaPalette[i] = chunk.getByte(i);
            }

            // Fill rest of palette with 255
            for (int i = entries; i < paletteEntries; i++) {
                alphaPalette[i] = (byte) 255;
            }

            if (!suppressAlpha) {
                if (expandPalette) {
                    postProcess = POST_PALETTE_TO_RGBA;
                    outputBands = 4;
                } else {
                    outputHasAlphaPalette = true;
                }
            }
        } else if (colorType == PNG_COLOR_GRAY) {
            grayTransparentAlpha = chunk.getInt2(0);

            if (!suppressAlpha) {
                if (bitDepth < 8) {
                    output8BitGray = true;
                    maxOpacity = 255;
                    postProcess = POST_GRAY_LUT_ADD_TRANS;
                } else {
                    postProcess = POST_ADD_GRAY_TRANS;
                }

                if (expandGrayAlpha) {
                    outputBands = 4;
                    postProcess |= POST_EXP_MASK;
                } else {
                    outputBands = 2;
                }

                if (encodeParam != null) {
                    ((PNGEncodeParam.Gray) encodeParam).setTransparentGray(grayTransparentAlpha);
                }
            }
        } else if (colorType == PNG_COLOR_RGB) {
            redTransparentAlpha = chunk.getInt2(0);
            greenTransparentAlpha = chunk.getInt2(2);
            blueTransparentAlpha = chunk.getInt2(4);

            if (!suppressAlpha) {
                outputBands = 4;
                postProcess = POST_ADD_RGB_TRANS;

                if (encodeParam != null) {
                    int[] rgbTrans = new int[3];
                    rgbTrans[0] = redTransparentAlpha;
                    rgbTrans[1] = greenTransparentAlpha;
                    rgbTrans[2] = blueTransparentAlpha;
                    ((PNGEncodeParam.RGB) encodeParam).setTransparentRGB(rgbTrans);
                }
            }
        } else if (colorType == PNG_COLOR_GRAY_ALPHA || colorType == PNG_COLOR_RGB_ALPHA) {
            // Error -- GA or RGBA image can't have a tRNS chunk.
            throw new RuntimeException(JaiI18N.getString("PNGImageDecoder15"));
        }
    }

    private void parse_zTXt_chunk(PNGChunk chunk) {
        String key = new String();
        String value = new String();
        byte b;

        int textIndex = 0;
        while ((b = chunk.getByte(textIndex++)) != 0) {
            key += (char) b;
        }
        int method = chunk.getByte(textIndex++);

        try {
            int length = chunk.getLength() - textIndex;
            byte[] data = chunk.getData();
            InputStream cis = new ByteArrayInputStream(data, textIndex, length);
            InputStream iis = new InflaterInputStream(cis);

            int c;
            while ((c = iis.read()) != -1) {
                value += (char) c;
            }

            ztextKeys.add(key);
            ztextStrings.add(value);
        } catch (Exception e) {
            ImagingListenerProxy.errorOccurred(JaiI18N.getString("PNGImageDecoder21"), e, this, false);
            //            e.printStackTrace();
        }
    }

    private WritableRaster createRaster(int width, int height, int bands, int scanlineStride, int bitDepth) {

        DataBuffer dataBuffer;
        WritableRaster ras = null;
        Point origin = new Point(0, 0);
        if ((bitDepth < 8) && (bands == 1)) {
            dataBuffer = new DataBufferByte(height * scanlineStride);
            ras = Raster.createPackedRaster(dataBuffer, width, height, bitDepth, origin);
        } else if (bitDepth <= 8) {
            dataBuffer = new DataBufferByte(height * scanlineStride);
            ras = Raster.createInterleavedRaster(
                    dataBuffer, width, height, scanlineStride, bands, bandOffsets[bands], origin);
        } else {
            dataBuffer = new DataBufferUShort(height * scanlineStride);
            ras = Raster.createInterleavedRaster(
                    dataBuffer, width, height, scanlineStride, bands, bandOffsets[bands], origin);
        }

        return ras;
    }

    // Data filtering methods

    private static void decodeSubFilter(byte[] curr, int count, int bpp) {
        for (int i = bpp; i < count; i++) {
            int val;

            val = curr[i] & 0xff;
            val += curr[i - bpp] & 0xff;

            curr[i] = (byte) val;
        }
    }

    private static void decodeUpFilter(byte[] curr, byte[] prev, int count) {
        for (int i = 0; i < count; i++) {
            int raw = curr[i] & 0xff;
            int prior = prev[i] & 0xff;

            curr[i] = (byte) (raw + prior);
        }
    }

    private static void decodeAverageFilter(byte[] curr, byte[] prev, int count, int bpp) {
        int raw, priorPixel, priorRow;

        for (int i = 0; i < bpp; i++) {
            raw = curr[i] & 0xff;
            priorRow = prev[i] & 0xff;

            curr[i] = (byte) (raw + priorRow / 2);
        }

        for (int i = bpp; i < count; i++) {
            raw = curr[i] & 0xff;
            priorPixel = curr[i - bpp] & 0xff;
            priorRow = prev[i] & 0xff;

            curr[i] = (byte) (raw + (priorPixel + priorRow) / 2);
        }
    }

    private static int paethPredictor(int a, int b, int c) {
        int p = a + b - c;
        int pa = Math.abs(p - a);
        int pb = Math.abs(p - b);
        int pc = Math.abs(p - c);

        if ((pa <= pb) && (pa <= pc)) {
            return a;
        } else if (pb <= pc) {
            return b;
        } else {
            return c;
        }
    }

    private static void decodePaethFilter(byte[] curr, byte[] prev, int count, int bpp) {
        int raw, priorPixel, priorRow, priorRowPixel;

        for (int i = 0; i < bpp; i++) {
            raw = curr[i] & 0xff;
            priorRow = prev[i] & 0xff;

            curr[i] = (byte) (raw + priorRow);
        }

        for (int i = bpp; i < count; i++) {
            raw = curr[i] & 0xff;
            priorPixel = curr[i - bpp] & 0xff;
            priorRow = prev[i] & 0xff;
            priorRowPixel = prev[i - bpp] & 0xff;

            curr[i] = (byte) (raw + paethPredictor(priorPixel, priorRow, priorRowPixel));
        }
    }

    private void processPixels(int process, Raster src, WritableRaster dst, int xOffset, int step, int y, int width) {
        int srcX, dstX;

        // Create an array suitable for holding one pixel
        int[] ps = src.getPixel(0, 0, (int[]) null);
        int[] pd = dst.getPixel(0, 0, (int[]) null);

        dstX = xOffset;
        switch (process) {
            case POST_NONE:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);
                    dst.setPixel(dstX, y, ps);
                    dstX += step;
                }
                break;

            case POST_GAMMA:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    for (int i = 0; i < inputBands; i++) {
                        int x = ps[i];
                        ps[i] = gammaLut[x];
                    }

                    dst.setPixel(dstX, y, ps);
                    dstX += step;
                }
                break;

            case POST_GRAY_LUT:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    pd[0] = grayLut[ps[0]];

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_GRAY_LUT_ADD_TRANS:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    pd[0] = grayLut[val];
                    if (val == grayTransparentAlpha) {
                        pd[1] = 0;
                    } else {
                        pd[1] = maxOpacity;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_PALETTE_TO_RGB:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    pd[0] = redPalette[val];
                    pd[1] = greenPalette[val];
                    pd[2] = bluePalette[val];

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_PALETTE_TO_RGBA:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    pd[0] = redPalette[val];
                    pd[1] = greenPalette[val];
                    pd[2] = bluePalette[val];
                    pd[3] = alphaPalette[val];

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_ADD_GRAY_TRANS:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    if (performGammaCorrection) {
                        val = gammaLut[val];
                    }
                    pd[0] = val;
                    if (val == grayTransparentAlpha) {
                        pd[1] = 0;
                    } else {
                        pd[1] = maxOpacity;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_ADD_RGB_TRANS:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int r = ps[0];
                    int g = ps[1];
                    int b = ps[2];
                    if (performGammaCorrection) {
                        pd[0] = gammaLut[r];
                        pd[1] = gammaLut[g];
                        pd[2] = gammaLut[b];
                    } else {
                        pd[0] = r;
                        pd[1] = g;
                        pd[2] = b;
                    }
                    if ((r == redTransparentAlpha) && (g == greenTransparentAlpha) && (b == blueTransparentAlpha)) {
                        pd[3] = 0;
                    } else {
                        pd[3] = maxOpacity;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_REMOVE_GRAY_TRANS:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int g = ps[0];
                    if (performGammaCorrection) {
                        pd[0] = gammaLut[g];
                    } else {
                        pd[0] = g;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_REMOVE_RGB_TRANS:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int r = ps[0];
                    int g = ps[1];
                    int b = ps[2];
                    if (performGammaCorrection) {
                        pd[0] = gammaLut[r];
                        pd[1] = gammaLut[g];
                        pd[2] = gammaLut[b];
                    } else {
                        pd[0] = r;
                        pd[1] = g;
                        pd[2] = b;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_GAMMA_EXP:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    int alpha = ps[1];
                    int gamma = gammaLut[val];
                    pd[0] = gamma;
                    pd[1] = gamma;
                    pd[2] = gamma;
                    pd[3] = alpha;

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_GRAY_ALPHA_EXP:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    int alpha = ps[1];
                    pd[0] = val;
                    pd[1] = val;
                    pd[2] = val;
                    pd[3] = alpha;

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_ADD_GRAY_TRANS_EXP:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    if (performGammaCorrection) {
                        val = gammaLut[val];
                    }
                    pd[0] = val;
                    pd[1] = val;
                    pd[2] = val;
                    if (val == grayTransparentAlpha) {
                        pd[3] = 0;
                    } else {
                        pd[3] = maxOpacity;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;

            case POST_GRAY_LUT_ADD_TRANS_EXP:
                for (srcX = 0; srcX < width; srcX++) {
                    src.getPixel(srcX, 0, ps);

                    int val = ps[0];
                    int val2 = grayLut[val];
                    pd[0] = val2;
                    pd[1] = val2;
                    pd[2] = val2;
                    if (val == grayTransparentAlpha) {
                        pd[3] = 0;
                    } else {
                        pd[3] = maxOpacity;
                    }

                    dst.setPixel(dstX, y, pd);
                    dstX += step;
                }
                break;
        }
    }

    /** Reads in an image of a given size and returns it as a WritableRaster. */
    private void decodePass(
            WritableRaster imRas, int xOffset, int yOffset, int xStep, int yStep, int passWidth, int passHeight) {
        if ((passWidth == 0) || (passHeight == 0)) {
            return;
        }

        int bytesPerRow = (inputBands * passWidth * bitDepth + 7) / 8;
        int eltsPerRow = (bitDepth == 16) ? bytesPerRow / 2 : bytesPerRow;
        byte[] curr = new byte[bytesPerRow];
        byte[] prior = new byte[bytesPerRow];

        // Create a 1-row tall Raster to hold the data
        WritableRaster passRow = createRaster(passWidth, 1, inputBands, eltsPerRow, bitDepth);
        DataBuffer dataBuffer = passRow.getDataBuffer();
        int type = dataBuffer.getDataType();
        byte[] byteData = null;
        short[] shortData = null;
        if (type == DataBuffer.TYPE_BYTE) {
            byteData = ((DataBufferByte) dataBuffer).getData();
        } else {
            shortData = ((DataBufferUShort) dataBuffer).getData();
        }

        // Decode the (sub)image row-by-row
        int srcY, dstY;
        for (srcY = 0, dstY = yOffset; srcY < passHeight; srcY++, dstY += yStep) {
            // Read the filter type byte and a row of data
            int filter = 0;
            try {
                filter = dataStream.read();
                dataStream.readFully(curr, 0, bytesPerRow);
            } catch (Exception e) {
                ImagingListenerProxy.errorOccurred(JaiI18N.getString("PNGImageDecoder2"), e, this, false);
                //                e.printStackTrace();
            }

            switch (filter) {
                case PNG_FILTER_NONE:
                    break;
                case PNG_FILTER_SUB:
                    decodeSubFilter(curr, bytesPerRow, bytesPerPixel);
                    break;
                case PNG_FILTER_UP:
                    decodeUpFilter(curr, prior, bytesPerRow);
                    break;
                case PNG_FILTER_AVERAGE:
                    decodeAverageFilter(curr, prior, bytesPerRow, bytesPerPixel);
                    break;
                case PNG_FILTER_PAETH:
                    decodePaethFilter(curr, prior, bytesPerRow, bytesPerPixel);
                    break;
                default:
                    // Error -- uknown filter type
                    throw new RuntimeException(JaiI18N.getString("PNGImageDecoder16"));
            }

            // Copy data into passRow byte by byte
            if (bitDepth < 16) {
                System.arraycopy(curr, 0, byteData, 0, bytesPerRow);
            } else {
                int idx = 0;
                for (int j = 0; j < eltsPerRow; j++) {
                    shortData[j] = (short) ((curr[idx] << 8) | (curr[idx + 1] & 0xff));
                    idx += 2;
                }
            }

            processPixels(postProcess, passRow, imRas, xOffset, xStep, dstY, passWidth);

            // Swap curr and prior
            byte[] tmp = prior;
            prior = curr;
            curr = tmp;
        }
    }

    private void decodeImage(boolean useInterlacing) {
        if (!useInterlacing) {
            decodePass(theTile, 0, 0, 1, 1, width, height);
        } else {
            decodePass(theTile, 0, 0, 8, 8, (width + 7) / 8, (height + 7) / 8);
            decodePass(theTile, 4, 0, 8, 8, (width + 3) / 8, (height + 7) / 8);
            decodePass(theTile, 0, 4, 4, 8, (width + 3) / 4, (height + 3) / 8);
            decodePass(theTile, 2, 0, 4, 4, (width + 1) / 4, (height + 3) / 4);
            decodePass(theTile, 0, 2, 2, 4, (width + 1) / 2, (height + 1) / 4);
            decodePass(theTile, 1, 0, 2, 2, width / 2, (height + 1) / 2);
            decodePass(theTile, 0, 1, 1, 2, width, height / 2);
        }
    }

    // RenderedImage stuff

    public Raster getTile(int tileX, int tileY) {
        if (tileX != 0 || tileY != 0) {
            // Error -- bad tile requested
            throw new IllegalArgumentException(JaiI18N.getString("PNGImageDecoder17"));
        }
        return theTile;
    }

    public void dispose() {
        theTile = null;
    }
}
